#include "IndiceBooleano.h"
#include "../ArchFrase/BloqueFrase.h"
#include "../BTree/RegistroDatos.h"
#include "../utils/FileSystem.h"
#include "../utils/Normalizador.h"
#include "../common/Resultados.h"
#include "../HeapSort/HeapSort.h"
#include "RegistroOcurrencia.h"
#include "IntersectorListas.h"
#include <algorithm>
#include <sstream>
#include <limits>


IndiceBooleano::IndiceBooleano() {
}

IndiceBooleano::~IndiceBooleano() {
	cerrar();
}


// Dadas las frases y los terminos (sin stop-words) crea el indice booleano
bool IndiceBooleano::crearIndices(ArchFrases& frases, BTree& terminosValidos, std::string nomArchTerminos, std::string nomArchListas){
	if (!frases.is_open()) return false;

	// Crea el árbol de terminos
	bool exito = this->terminos.crear(nomArchTerminos);
	if (!exito) return false;

	// Recorre el arch de frases y construye el de ocurrencias por terminos
	std::string nomArchOcurrencias = construirArchOcurrenciasTerminos(frases, terminosValidos);
	if (nomArchOcurrencias == "") return false;

	// Ordena el archivo de ocurrencias y guarda el resultado en "nomArchOcurrenciasOrdenado"
	std::string nomArchOcurrenciasOrdenado = FileSystem::getNombreArchivoTemporal();

	HeapSort hs(nomArchOcurrencias);
	hs.init(nomArchOcurrenciasOrdenado);
	hs.sort();

	// Crea las listas invertidas
	this->listaInvertida.crear(nomArchListas);
	construirListasInvertidas(nomArchOcurrenciasOrdenado);

	// Borra los archivos temporales, ya no me sirven
	FileSystem::eliminarArchivo(nomArchOcurrencias);
	FileSystem::eliminarArchivo(nomArchOcurrenciasOrdenado);
	return true;
}


// Construye el archivo de ocurrencias de terminos y devuelve el nombre del mismo
std::string IndiceBooleano::construirArchOcurrenciasTerminos(ArchFrases& frases, BTree& terminosValidos){

	// El nombre de un archivo temporal para almacenar las ocurrencias de terminos
	std::string nomArchOcurrenciaTermino = FileSystem::getNombreArchivoTemporal();
	std::fstream archOcurrencias(nomArchOcurrenciaTermino.c_str(), std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::trunc);
	if (!archOcurrencias) return "";

	// Recorre el archivo de frases
	for (unsigned int docID = 0; docID < frases.getCantBloques()-1; ++docID){
		BloqueFrase bf;
		frases.leerFrase(bf, docID);	// NOTA: El docID puede ser cero, pero no el valor que se almacena!!

		// Separa la frase en terminos
		std::vector<std::string> vecTerm = procesarTerminosFrase(bf.getFrase(), terminosValidos);

		// Para cada termino en la frase
		std::vector<std::string>::const_iterator cit;
		for (cit = vecTerm.begin(); cit != vecTerm.end(); ++cit){
			/** Según las diapositivas se deben guardar los ID de terminos en el archivo de ocurrencias para que al realizar el
			 * ordenamiento solo se esten comparando enteros que son pequeños y su comparacion es muy rápida.
			 *
			 * Desafortunadamente en este TP no tenemos la estructura para recuperar el registro que contiene el termino del árbol de
			 * terminos dado su ID de termino ya que el árbol se ordena en base a la cadena de caracteres que conforma el mismo termino.
			 * Por esto vamos a almacenar, ordenar y construir las listas invertivas usando los terminos y no sus Ids
			 *
			 * XXX: Si tengo tiempo antes de la entrega veo una forma de solucionar este problema... capaz con un indice secundario
			 * */
			// Se almacena el termino en el arbol de terminos
			almacenarTerminoSinID(*cit);

			// Almacena el par en un archivo de ocurrencias de terminos, este queda ordenado por ID de documento
			RegistroOcurrencia rOcurr(*cit, docID+1);	// PORQUE NO PUEDE EXISTIR UN ID DE DOCUMENTO CON VALOR CERO sumo uno a todos
			archOcurrencias << rOcurr;
		}
	}

	// Cierra el archivo temporal de ocurrencias y devuelve su nombre
	archOcurrencias.close();
	return nomArchOcurrenciaTermino;
}


// Recorre el archivo de ocurrencias de terminos ordenados y crea las listas invertidas
void IndiceBooleano::construirListasInvertidas(std::string nomOcurrenciasOrdenado){
	// Recorre el archivo de "ocurrencia de terminos ordenados"
	std::fstream archOcurrenciasOrdenado(nomOcurrenciasOrdenado.c_str(), std::fstream::in | std::fstream::binary);

	std::string terminoAnterior = "";
	std::vector<unsigned int> vecInvertido;

	while (!archOcurrenciasOrdenado.eof()){

		RegistroOcurrencia rOcurr;
		archOcurrenciasOrdenado >> rOcurr;

		// Si el termino actual es distinto al anterior ya se terminó la construccion de la lista invertida
		if (rOcurr.getTermino() != terminoAnterior && terminoAnterior != "" && !archOcurrenciasOrdenado.eof()){

			// Almacena la lista invertida y la asocia al termino anterior en el árbol
			unsigned int nroBloque = AlmacenarListaInvertida(vecInvertido);
			updateTermino(terminoAnterior, nroBloque);

			// Elimina todo el contenido del vector para el proximo termino
			vecInvertido.clear();
		}

		// Va armando la lista invertida de documentos para el termino
		vecInvertido.push_back(rOcurr.getDocID());
		terminoAnterior = rOcurr.getTermino();
	}

	archOcurrenciasOrdenado.close();
}


// Guarda la lista en un bloque del archivo de listas invertidas y devuelve su nro de bloque
unsigned int IndiceBooleano::AlmacenarListaInvertida(std::vector<unsigned int>& lDocsID){
	BloqueListaInvertida bli;
	bli.guardarIDs(lDocsID);

	unsigned int posBloque = this->listaInvertida.guardarLista(bli);
	return posBloque;
}


void IndiceBooleano::updateTermino(std::string termino, unsigned int nroBloque){
	RegistroDatos* rGuardar = new RegistroDatos(termino, nroBloque);
	this->terminos.modificarRegistro(rGuardar);
}


// Por si ya existen los archivos, se pueden abrir
bool IndiceBooleano::abrirIndices(std::string nomArchTerminos, std::string nomArchListas){
	this->listaInvertida.abrir(nomArchListas);
	bool exito1 = this->terminos.abrir(nomArchTerminos);

	return exito1;
}


void IndiceBooleano::cerrar(){
	if (this->terminos.is_open()) this->terminos.cerrar();
	if (this->listaInvertida.is_open()) this->listaInvertida.cerrar();
}

//--------------------------------------------------------------------------------------------------

// Dada una lista de terminos devuelve una lista de "ID" de documentos
std::vector<unsigned int> IndiceBooleano::buscarDocumentos(std::vector<std::string> &lTerminos) {
	if (!this->terminos.is_open() || !this->listaInvertida.is_open())
		throw std::runtime_error("El indice no esta cargado todavia");

	// Guarda los bloques con las listas invertidas
	std::vector<BloqueListaInvertida> vecListas;

	// Recupera la listas de documentos para cada termino
	std::vector<std::string>::const_iterator it;
	for (it = lTerminos.begin(); it != lTerminos.end(); ++it){
		// Uso el campo reservado para ver si encontré o no el termino
		RegistroDatos r1(*it, 0, std::numeric_limits<unsigned short>::max());
		RegistroDatos r2 = this->terminos.buscarRegistro(r1);

		// Solo proceso la lista si se encontró el termino
		if (r2.getClave() == r1.getClave()){
			// Recupera la lista invertida
			BloqueListaInvertida bli;
			this->listaInvertida.leerLista(bli, r2.getPosEnDiccionario());
			vecListas.push_back(bli);
		}
	}

	// Interseccion de listas
	std::vector<unsigned int> vecIDs = intersectarListasInvertidas(vecListas);
	normalizarIDs(vecIDs);
	return vecIDs;
}


// Los documentos empiezan en cero, pero no se puede guardar cero en el árbol de terminos
// Esta funcion soluciona el problema
void IndiceBooleano::normalizarIDs(std::vector<unsigned int> &vec) const{
	/** Como se suma 1 (uno) a los ids de los documentos al guardar los terminos, para que funcione la
	 * funcion de compresion Gamma, esta funcion resta ese mismo valor para que los documentos matchen
	 * */
	std::vector<unsigned int>::iterator it;
	for (it = vec.begin(); it != vec.end(); ++it) --(*it);
}


// Realiza la intersccion de las listas de documentos
std::vector<unsigned int> IndiceBooleano::intersectarListasInvertidas(std::vector<BloqueListaInvertida> &vecDocs){
	std::vector< std::vector<unsigned int> > vecListasInvertidas;

	// Extrae todas las listas
	std::vector<BloqueListaInvertida>::iterator it;
	for (it = vecDocs.begin(); it != vecDocs.end(); ++it){
		std::vector<unsigned int> vecTemp;
		it->recuperarIDs(vecTemp);

		vecListasInvertidas.push_back(vecTemp);
	}

	// Carga las listas en el itersector y devuelve la interseccion de las mismas
	IntersectorListas il;
	std::vector< std::vector<unsigned int> >::iterator it2;
	for (it2 = vecListasInvertidas.begin(); it2 != vecListasInvertidas.end(); ++it2){
		il.addLista(*it2);
	}
	std::vector<unsigned int> vecIntersecciones = il.getInterseccion();

	return vecIntersecciones;
}


void IndiceBooleano::altaDocumento(std::string frase, unsigned int docID, BTree& terminosValidos){
	// Separa la frase en terminos
	std::vector<std::string> vecTerm = procesarTerminosFrase(frase, terminosValidos);

	// Para cada termino en la frase se recupera su lista invertida
	std::vector<std::string>::const_iterator cit;
	for (cit = vecTerm.begin(); cit != vecTerm.end(); ++cit){

		// Busca el termino
		RegistroDatos rTerm(*cit, std::numeric_limits<unsigned int>::max());
		RegistroDatos rEncotrado = this->terminos.buscarRegistro(rTerm);

		if (rEncotrado.getPosEnDiccionario() != rTerm.getPosEnDiccionario()){
			// Recupera la lista invertida del mismo
			BloqueListaInvertida bli;
			this->listaInvertida.leerLista(bli, rEncotrado.getPosEnDiccionario());

			// Agregar el nuevo ID y lo guarda
			bli.agregarID(docID);
			this->listaInvertida.guardarLista(bli, rEncotrado.getPosEnDiccionario());

		} else {
			// Es un termino nuevo en el árbol, creo que deberia registrarse
			throw std::runtime_error("Nuevo termino de una nueva frase " + *cit);
		}
	}
}


void IndiceBooleano::bajaDocumento(std::string frase, unsigned int docID, BTree& terminosValidos){
	// Separa la frase en terminos
	std::vector<std::string> vecTerm = procesarTerminosFrase(frase, terminosValidos);

	// Para cada termino en la frase se recupera su lista invertida
	std::vector<std::string>::const_iterator cit;
	for (cit = vecTerm.begin(); cit != vecTerm.end(); ++cit){

		// Busca el termino
		RegistroDatos rTerm(*cit, std::numeric_limits<unsigned int>::max());
		RegistroDatos rEncotrado = this->terminos.buscarRegistro(rTerm);

		if (rEncotrado.getPosEnDiccionario() != rTerm.getPosEnDiccionario()){
			// Recupera la lista invertida del mismo
			BloqueListaInvertida bli;
			this->listaInvertida.leerLista(bli, rEncotrado.getPosEnDiccionario());

			// Se quita el ID y guarda los cambios
			bli.eliminarID(docID+1);	// Porque el +1? por la forma del archivo de frases
			this->listaInvertida.guardarLista(bli, rEncotrado.getPosEnDiccionario());

		} else {
			// Es un termino nuevo en el árbol, de una frase que ya existia, no deberia pasar
			throw std::runtime_error("Nuevo termino de una frase vieja " + *cit);
		}
	}
}

//--------------------------------------------------------------------------------------------------

// Dada una frase la separa en palabras y chequea si estas palabras son terminos del vocabulario o stop-words
std::vector<std::string> IndiceBooleano::procesarTerminosFrase(std::string frase, BTree& terminos) const{

	std::vector<std::string> vec;
	std::stringstream ss(frase);

	while ( !ss.eof() ){
		std::string palabra = "";
		ss >> palabra;

		// Se normaliza la palabra y si no es una palabra hay que evitar guardarla
		palabra = Normalizador::normalizarPalabra(palabra);
		if (palabra != ""){
			// Si la palabra esta NO en el árbol es una stop-word
			RegistroDatos r1(palabra, std::numeric_limits<unsigned int>::max());
			RegistroDatos r2 = terminos.buscarRegistro(r1);

			// Al darle un valor imposible al campo "posDicc" podemos chequear si la palabra esta o no en el árbol
			if (r1.getPosEnDiccionario() != r2.getPosEnDiccionario()){
				vec.push_back(palabra);
			}
		}
	}

	// Ordeno y quito los duplicados
	std::sort(vec.begin(), vec.end());
	std::vector<std::string>::iterator itDuplicados;
	itDuplicados = std::unique(vec.begin(), vec.end());
	if (itDuplicados != vec.end()) vec.erase(itDuplicados, vec.end());

	return vec;
}


// Almacena el termino en el arbol si este no existe, devuelve el ID del termino en cualquier caso
unsigned int IndiceBooleano::almacenarTermino(std::string termino){
	// Va generando y creando los id de terminos de cada termino
	static unsigned int s_ID_TERMINO = 0;

	// Trata de insertar el termino con un nuevo ID de termino, como la comparacion se hace por termino no importa el valor del ID
	RegistroDatos* r = new RegistroDatos(termino, ++s_ID_TERMINO);
	Resultado resul = this->terminos.insertarRegistro(r);

	// Si logró insertar el registro significa que el termino es nuevo y ya tengo su ID de termino
	if (resul != CLAVE_DUPLICADA) return s_ID_TERMINO;

	// No logró insertar el registro, significa que este ya existe y hay que recuperar su ID de termino
	RegistroDatos rTerm = this->terminos.buscarRegistro(*r);

	// Hay que liberar el registro creado y devolver el ID del termino
	delete r;
	return rTerm.getPosEnDiccionario();
}


// Realiza la insercion del termino en el arbol, si este ya existe no pasa nada
void IndiceBooleano::almacenarTerminoSinID(std::string termino){
	RegistroDatos* r = new RegistroDatos(termino, 0);
	Resultado res = this->terminos.insertarRegistro(r);
	if (res == CLAVE_DUPLICADA) delete r;
}

/*
// Almacena el par en un archivo de ocurrencias de terminos
void IndiceBooleano::almacenarOcurrenciaTermino(unsigned int docID, unsigned int termID, std::fstream &arch) const{
	arch.write(reinterpret_cast<char*>(&docID), sizeof(docID));
	arch.write(reinterpret_cast<char*>(&termID), sizeof(termID));
}


void IndiceBooleano::almacenarOcurrenciaTermino(unsigned int docID, std::string termino, std::fstream &arch) const{
	unsigned char tamTerm = static_cast<unsigned char>( termino.length() );

	arch.write(reinterpret_cast<char*>(&docID), sizeof(docID));
	arch.write(reinterpret_cast<char*>(&tamTerm), sizeof(tamTerm));
	arch.write(termino.c_str(), termino.length());
}
*/
